/*
 * Network.java
 *
 * Created on Dec 1, 2011
 *
 * Copyright (c) 2011 Artur Rataj.
 *
 * This code is distributed under the terms of the GNU Library
 * General Public License, either version 3 of the license or, at
 * your option, any later version.
 */

package pl.gliwice.iitis.spatialnn.network;

import java.util.*;

import pl.gliwice.iitis.graphutils.Point;
import pl.gliwice.iitis.graphutils.Rect;
import pl.gliwice.iitis.graphutils.view.Drawable;

/**
 * A network.
 * 
 * @author Artur Rataj
 */
public class Network implements Drawable {
    /**
     * Size of this network.
     */
    public Point size;
    /**
     * Membranes in this network.
     */
    public List<Membrane> membranes;
    /**
     * Neurons in this network.
     */
    public List<Neuron> neurons;
    /**
     * Membranes in this network, keyed with their keys.<br>
     * 
     * This map contains the same membranes as <code>membranes</code>.
     */
    public Map<String, Membrane> membraneMap;
    /**
     * Neurons in this network, keyed with their keys.<br>
     * 
     * This map contains the same neurons as <code>neurons</code>.
     */
    public Map<Integer, Neuron> neuronMap;
    /**
     * Maximum possible number of a layer, -1 for an empty network.
     */
    public int maxLayerNum;
    /**
     * If the turn zone exists, this is set to the zone's bound after the construction.
     */
    public Rect turnBound = null;

    public double symmetricInputSpread = Double.NaN;
    
    /**
     * Creates a new instance of <code>Network</code>.
     */
    public Network() {
        maxLayerNum = -1;
    }
    /**
     * Adds a neuron to <code>neurons</code> and <code>neuronMap</code>,
     * updates <code>maxLayerNum</code>.
     * 
     * @param n neuron to add
     */
    public void add(Neuron n) {
        neurons.add(n);
        neuronMap.put(n.key, n);
        if(maxLayerNum < n.layerNum)
            maxLayerNum = n.layerNum;
    }
    /**
     * Returns maximum index of a layer in this network.
     * 
     * @return maximum possible number of a layer, -1 if this network is empty
     */
    public int getMaxLayer() {
        return maxLayerNum;
    }
    /**
     * Returns this network's component, identified by a given target.
     * 
     * @param target target, representing the component to return
     * @return a component of this network
     */
    public Component getComponent(AbstractTarget target) {
        if(target instanceof MembraneTarget) {
            MembraneTarget m = (MembraneTarget)target;
            return membraneMap.get(m.key);
        } else if(target instanceof NeuronTarget) {
            NeuronTarget n = (NeuronTarget)target;
            return neuronMap.get(n.key);
        } else
            throw new RuntimeException("unknown type of target");
    }
    /**
     * Returns the output membrane.
     * 
     * This network needs to have only a single output membrane for this method to work,
     * otherwise a runtime exception is thrown.
     * 
     * @return the output membrane of this network
     */
    public Membrane getOutMembrane() {
        Membrane out = null;
        for(Membrane m : membranes) {
            if(m.type == MembraneType.OUT) {
                if(out != null)
                    throw new RuntimeException("only networks with a single output " +
                            "membrane are supported");
                out = m;
            }
        }
        return out;
    }
    /**
     * Returns the type of this network: straight, turned or mixed. The type depends
     * on the placement of membranes.
     * 
     * @return type of this network
     */
    public NetworkType getType() {
        final double EPSILON = 1e-6;
        double inAngle = Double.NaN;
        double outAngle = Double.NaN;
        double angle;
        for(Membrane m : membranes)
            switch(m.type) {
                case IN:
                    angle = m.getAngle();
                    if(!Double.isNaN(inAngle) &&
                            Math.abs(inAngle - angle) > EPSILON)
                        return NetworkType.MIXED;
                    inAngle = angle;
                    break;
                    
                case OUT:
                    angle = m.getAngle();
                    if(!Double.isNaN(outAngle) &&
                            Math.abs(outAngle - angle) > EPSILON)
                        return NetworkType.MIXED;
                    outAngle = angle;
                    break;
                    
                default:
                    throw new RuntimeException("unknown membrane type");
            }
        double diff = Math.abs(outAngle - inAngle);
        if(diff < EPSILON)
            return NetworkType.STRAIGHT;
        else if(Math.abs(diff - Math.PI/2.0) < EPSILON)
            return NetworkType.TURNED;
        else
            return NetworkType.MIXED;
    }
    /**
     * Checks, if connections are defined at both ends, and membranes have
     * correct types connection--wise.
     * 
     * Let A and B be either a neuron or a membrane. If A declares B as its
     * output, then B should list A as its input.
     * 
     * @return null if the connectivity is valid, otherwise an error message
     */
    public String checkConnectivity() {
        String out = "";
        for(Membrane n : membranes)
            for(AbstractTarget t : n.connections)
                switch(n.type) {
                    case IN:
                    {
                        if(t instanceof NeuronTarget) {
                            NeuronTarget nt = (NeuronTarget)t;
                            Neuron m = neuronMap.get(nt.key);
                            boolean found = false;
                            for(AbstractTarget u : m.inputs)
                                if(u instanceof MembraneTarget) {
                                    MembraneTarget nu = (MembraneTarget)u;
                                    if(nu.key.equals(n.key)) {
                                        found = true;
                                        break;
                                    }
                                }
                            if(!found)
                                out += "membrane " + n.key + " declares output to neuron " + m.key + ", " +
                                        "but reverse declaration is absent\n";
                        } else
                            out += "membrane " + n.key + " connects to a non--neuron\n";
                        break;
                    }
                    case OUT:
                    {
                        if(t instanceof NeuronTarget) {
                            NeuronTarget nt = (NeuronTarget)t;
                            Neuron m = neuronMap.get(nt.key);
                            boolean found = false;
                            for(AbstractTarget u : m.output)
                                if(u instanceof MembraneTarget) {
                                    MembraneTarget nu = (MembraneTarget)u;
                                    if(nu.key.equals(n.key)) {
                                        found = true;
                                        break;
                                    }
                                }
                            if(!found)
                                out += "membrane " + n.key + " declares input from neuron " + m.key + ", " +
                                        "but reverse declaration is absent\n";
                        } else
                            out += "membrane " + n.key + " connects to a non--neuron\n";
                        break;
                    }
                    default:
                        throw new RuntimeException("unknown membrane type");
                }
        for(Neuron n : neurons) {
            for(AbstractTarget t : n.inputs) {
                if(t instanceof NeuronTarget) {
                    NeuronTarget nt = (NeuronTarget)t;
                    Neuron m = neuronMap.get(nt.key);
                    boolean found = false;
                    for(AbstractTarget u : m.output)
                        if(u instanceof NeuronTarget) {
                            NeuronTarget nu = (NeuronTarget)u;
                            if(nu.key == n.key) {
                                found = true;
                                break;
                            }
                        }
                    if(!found)
                        out += "neuron " + n.key + " declares input from neuron " + m.key + ", " +
                                "but reverse declaration is absent\n";
                } else if(t instanceof MembraneTarget) {
                    MembraneTarget nt = (MembraneTarget)t;
                    Membrane m = membraneMap.get(nt.key);
                    if(m.type != MembraneType.IN)
                        out += "neuron " + n.key + " declares input from non--input membrane " +
                                m.key + "\n";
                    boolean found = false;
                    for(AbstractTarget u : m.connections)
                        if(u instanceof NeuronTarget) {
                            NeuronTarget nu = (NeuronTarget)u;
                            if(nu.key == n.key) {
                                found = true;
                                break;
                            }
                        }
                    if(!found)
                        out += "neuron " + n.key + " declares input from membrane " + m.key + ", " +
                                "but reverse declaration is absent\n";
                }
            }
            for(AbstractTarget t : n.output) {
                if(t instanceof NeuronTarget) {
                    NeuronTarget nt = (NeuronTarget)t;
                    Neuron m = neuronMap.get(nt.key);
                    boolean found = false;
                    if(m == null)
                        m = m;
                    for(AbstractTarget u : m.inputs)
                        if(u instanceof NeuronTarget) {
                            NeuronTarget nu = (NeuronTarget)u;
                            if(nu.key == n.key) {
                                found = true;
                                break;
                            }
                        }
                    if(!found)
                        out += "neuron " + n.key + " declares output to neuron " + m.key + ", " +
                                "but reverse declaration is absent\n";
                } else if(t instanceof MembraneTarget) {
                    MembraneTarget nt = (MembraneTarget)t;
                    Membrane m = membraneMap.get(nt.key);
                    if(m.type != MembraneType.OUT)
                        out += "neuron " + n.key + " declares output to non--output membrane " +
                                m.key + "\n";
                    boolean found = false;
                    for(AbstractTarget u : m.connections)
                        if(u instanceof NeuronTarget) {
                            NeuronTarget nu = (NeuronTarget)u;
                            if(nu.key == n.key) {
                                found = true;
                                break;
                            }
                        }
                    if(!found)
                        out += "neuron " + n.key + " declares output to membrane " + m.key + ", " +
                                "but reverse declaration is absent\n";
                }
            }
        }
        return out.isEmpty() ? null : out;
    }
    /**
     * Returns the neurons specifically in a given layer.
     * 
     * @param layerNum number of layer
     * @return neurons in the layer, empty list for none
     */
    public List<Neuron> getLayerNeurons(int layerNum) {
        List<Neuron> out = new LinkedList<>();
        for(Neuron n : neurons)
            if(n.layerNum == layerNum)
                out.add(n);
        return out;    
    }
    /**
     * If there is an input membrane keyed "left", it is returned. Otherwise, null
     * is returned.
     * 
     * @return an unique left membrane or null
     */
    public Membrane getLeftMembrane() {
        Membrane left = null;
        for(Membrane m : membranes)
            if(m.type == MembraneType.IN && m.key.equals("left")) {
                if(left != null)
                    throw new RuntimeException("multiple membranes with the same key");
                left = m;
            }
        return left;
    }
    /**
     * If there is an input membrane keyed "right", it is returned. Otherwise, null
     * is returned.
     * 
     * @return an unique right membrane or null
     */
    public Membrane getRightMembrane() {
        Membrane right = null;
        for(Membrane m : membranes)
            if(m.type == MembraneType.IN && m.key.equals("right")) {
                if(right != null)
                    throw new RuntimeException("multiple membranes with the same key");
                right = m;
            }
        return right;
    }
    /**
     * If there is a single input membrane, it is returned. Otherwise, null
     * is returned.
     * 
     * @return an unique left membrane or null
     */
    public Membrane getInputMembrane() {
        Membrane common = null;
        for(Membrane m : membranes)
            if(m.type == MembraneType.IN) {
                if(common != null)
                    return null;
                common = m;
            }
        return common;
    }
    /**
     * Synchronises targets. Currently sets <code>NeuronTarget.index</code>.
     */
    protected void syncTargets() {
        //
        // index connections to neurons
        //
        for(Neuron n : neurons) {
            int num = n.inputs.length;
            if(n.key == 263)
                n = n;
            // already `taken' connections, to distinguish two connections
            // between the same components and with the same expression
            Set<AbstractTarget> used = new HashSet<>();
            for(int count = 0; count < num; ++count) {
                AbstractTarget u = n.inputs[count];
                Component from = getComponent(u);
                boolean found = false;
                if(from instanceof Membrane) {
                    double expression = ((MembraneTarget)u).expression;
                    Membrane m = (Membrane)from;
                    int index = m.connections.length;
                    for(int i = 0; i < index; ++i) {
                        AbstractTarget t = m.connections[i];
                        double e = m.expressions[i];
                        Component to = getComponent(t);
                        if(to == n && expression == e && !used.contains(t)) {
                            NeuronTarget nT = (NeuronTarget)t;
                            nT.index = count;
                            used.add(t);
                            found = true;
                            break;
                        }
                    }
                } else if(from instanceof Neuron) {
                    Neuron v = (Neuron)from;
                    int index = v.output.length;
                    for(int i = 0; i < index; ++i) {
                        AbstractTarget t = v.output[i];
                        Component to = getComponent(t);
                        if(to == n && !used.contains(t)) {
                            NeuronTarget nT = (NeuronTarget)t;
                            nT.index = count;
                            used.add(t);
                            found = true;
                            break;
                        }
                    }
                } else
                    throw new RuntimeException("unknown type of component");
                if(!found)
                    throw new RuntimeException("target in the other component not found");
            }
        }
        //
        // index connections to the output membrane
        //
        Membrane m = getOutMembrane();
        int num = m.connections.length;
        // already `taken' connections, to distinguish two connections
        // between the same components and with the same expression
        Set<AbstractTarget> used = new HashSet<>();
        for(int count = 0; count < num; ++count) {
            AbstractTarget u = m.connections[count];
            Component from = getComponent(u);
            boolean found = false;
            if(from instanceof Neuron) {
                Neuron v = (Neuron)from;
                int index = v.output.length;
                for(int i = 0; i < index; ++i) {
                    AbstractTarget t = v.output[i];
                    Component to = getComponent(t);
                    if(to == m && !used.contains(t)) {
                        MembraneTarget mT = (MembraneTarget)t;
                        mT.index = count;
                        used.add(t);
                        found = true;
                        break;
                    }
                }
            } else
                throw new RuntimeException("memberane can be connected only to neurons");
            if(!found)
                throw new RuntimeException("target in the other component not found");
        }
    }
    /**
     * Synchronises all components and targets in this network.<br>
     * 
     * Nedded for propagation.
     */
    public void sync() {
        for(Membrane m : membranes)
            m.sync(this);
        for(Neuron n : neurons)
            n.sync(this);
        syncTargets();
    }
}
